diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..dcedc841 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +custom: ['https://crowdfundraising.ubc.ca/projects/ubc-thunderbots/'] diff --git a/.gitignore b/.gitignore index 0c328630..89bc960d 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,229 @@ vhdl/thunderbots_vhdl.prj vhdl/work-obj93.cf vhdl/xlnx_auto_0_xdb vhdl/xst + + +# Created by https://www.gitignore.io/api/venv,pycharm +# Edit at https://www.gitignore.io/?templates=venv,pycharm + +### PyCharm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PyCharm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +.idea/**/sonarlint/ + +# SonarQube Plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator/ + + +# Created by https://www.gitignore.io/api/python +# Edit at https://www.gitignore.io/?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +### venv ### +# Virtualenv +# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ +.Python +[Bb]in +[Ii]nclude +[Ll]ib +[Ll]ib64 +[Ll]ocal +[Ss]cripts +pyvenv.cfg +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +pip-selfcheck.json + +# End of https://www.gitignore.io/api/venv,pycharm + +# Mergetool files +*.orig diff --git a/.idea/Electrical.iml b/.idea/Electrical.iml new file mode 100644 index 00000000..c98d1518 --- /dev/null +++ b/.idea/Electrical.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..105ce2da --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..eb9a4e16 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..f226b1f2 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..94a25f7f --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/models/MagWire/.idea/MagWire.iml b/models/MagWire/.idea/MagWire.iml new file mode 100644 index 00000000..85c7612b --- /dev/null +++ b/models/MagWire/.idea/MagWire.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/models/MagWire/.idea/inspectionProfiles/profiles_settings.xml b/models/MagWire/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..105ce2da --- /dev/null +++ b/models/MagWire/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/models/MagWire/.idea/misc.xml b/models/MagWire/.idea/misc.xml new file mode 100644 index 00000000..3d64936d --- /dev/null +++ b/models/MagWire/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/models/MagWire/.idea/modules.xml b/models/MagWire/.idea/modules.xml new file mode 100644 index 00000000..194e2f29 --- /dev/null +++ b/models/MagWire/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/models/MagWire/.idea/vcs.xml b/models/MagWire/.idea/vcs.xml new file mode 100644 index 00000000..b2bdec2d --- /dev/null +++ b/models/MagWire/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/models/MagWire/.idea/workspace.xml b/models/MagWire/.idea/workspace.xml new file mode 100644 index 00000000..a12dc72b --- /dev/null +++ b/models/MagWire/.idea/workspace.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1581798536045 + + + + + + + \ No newline at end of file diff --git a/models/MagWire/biotsavart.py b/models/MagWire/biotsavart.py new file mode 100644 index 00000000..561be496 --- /dev/null +++ b/models/MagWire/biotsavart.py @@ -0,0 +1,110 @@ +__author__ = 'wack' + +# part of the magwire package + +# calculate magnetic fields arising from electrical current through wires of arbitrary shape +# with the law of Biot-Savart + +# written by Michael Wack +# wack@geophysik.uni-muenchen.de + +# tested with python 3.4.3 + + +import numpy as np +import time +#import multiprocessing as mp +from matplotlib import pyplot as plt +import mpl_toolkits.mplot3d.axes3d as ax3d + +class BiotSavart: + ''' + calculates the magnetic field generated by currents flowing through wires + ''' + + def __init__(self, wire=None): + self.wires = [] + if wire is not None: + self.wires.append(wire) + + def AddWire(self, wire): + self.wires.append(wire) + + def CalculateB(self, points): + """ + calculate magnetic field B at given points + :param points: numpy array of n points (xyz) + :return: numpy array of n vectors representing the B field at given points + """ + + print("found {} wire(s).".format(len(self.wires))) + c = 0 + # generate list of IdL and r1 vectors from all wires + for w in self.wires: + c += 1 + _IdL, _r1 = w.IdL_r1 + print("wire {} has {} segments".format(c, len(_IdL))) + if c == 1: + IdL = _IdL + r1 = _r1 + else: + IdL = np.vstack((IdL, _IdL)) + r1 = np.vstack((r1, _r1)) + print("total number of segments: {}".format(len(IdL))) + print("number of field points: {}".format(len(points))) + print("total number of calculations: {}".format(len(points)*len(IdL))) + + # now we have + # all segment vectors multiplied by the flowing current in IdL + # and all vectors to the central points of the segments in r1 + + # calculate vector B*1e7 for each point in space + t1 = time.process_time() + # simple list comprehension to calculate B at each point r + B = np.array([BiotSavart._CalculateB1(r, IdL, r1) * 1e-7 for r in points]) + + # multi processing + # slower than single processing? + #pool = mp.Pool(processes=16) + #B = np.array([pool.apply(_CalculateB1, args=(r, IdL, r1)) for r in points]) + + t2 = time.process_time() + print("time needed for calculation: {} s".format(t2-t1)) + + return B + + def vv_PlotWires(self): + for w in self.wires: + w.vv_plot_path() + + def mpl3d_PlotWires(self, ax): + for w in self.wires: + w.mpl3d_plot_path(show=False, ax=ax) + + + + + @staticmethod + def _CalculateB1(r, IdL, r1): + ''' + calculate magnetic field B for one point r in space + :param r: 3 component numpy array representing the location where B will be calculated + :param IdL: all segment vectors multiplied by the flowing current + :param r1: all vectors to the central points of the segments + :return: numpy array of 3 component vector of B multiplied by 1e7 + ''' + + # calculate law of biot savart for all current elements at given point r + r2 = r - r1 + r25 = np.linalg.norm(r2, axis=1)**3 + r3 = r2 / r25[:, np.newaxis] + + cr = np.cross(IdL, r3) + + # claculate sum of contributions from all current elements + s = np.sum(cr, axis=0) + + return s + + + diff --git a/models/MagWire/magwire.py b/models/MagWire/magwire.py new file mode 100644 index 00000000..8d78b4d3 --- /dev/null +++ b/models/MagWire/magwire.py @@ -0,0 +1,63 @@ +__author__ = 'wack' + +# part of the magwire package + +# calculate magnetic fields arising from electrical current through wires of arbitrary shape +# with the law of Biot-Savart + +# written by Michael Wack 2015 +# wack@geophysik.uni-muenchen.de + +# tested with python 3.4.3 + +# some basic calculations for testing + + +import numpy as np + +import visvis as vv + +import wire +import biotsavart + + +# simple solenoid +w = wire.Wire(path=wire.Wire.SolenoidPath(), discretization_length=0.01, current=100).Translate((0.1, 0.1, 0)).Rotate(axis=(1, 0, 0), deg=45) +sol = biotsavart.BiotSavart(wire=w) + +resolution = 0.02 +volume_corner1 = (-.2, -.3, -.2) +volume_corner2 = (.3, .3, .4) + +grid = np.mgrid[volume_corner1[0]:volume_corner2[0]:resolution, volume_corner1[1]:volume_corner2[1]:resolution, volume_corner1[2]:volume_corner2[2]:resolution] + +# create list of grid points +points = np.vstack(map(np.ravel, grid)).T + +# calculate B field at given points +B = sol.CalculateB(points=points) + +Babs = np.linalg.norm(B, axis=1) + +# draw results + +# prepare axes +a = vv.gca() +a.cameraType = '3d' +a.daspectAuto = False + +vol = Babs.reshape(grid.shape[1:]).T +vol = np.clip(vol, 0.002, 0.01) +vol = vv.Aarray(vol, sampling=(resolution, resolution, resolution), origin=(volume_corner1[2], volume_corner1[1], volume_corner1[0])) + +# set labels +vv.xlabel('x axis') +vv.ylabel('y axis') +vv.zlabel('z axis') + +sol.vv_PlotWires() + +t = vv.volshow2(vol, renderStyle='mip', cm=vv.CM_JET) +vv.colorbar() +app = vv.use() +app.Run() diff --git a/models/MagWire/palint_oven.py b/models/MagWire/palint_oven.py new file mode 100644 index 00000000..5d3db58c --- /dev/null +++ b/models/MagWire/palint_oven.py @@ -0,0 +1,81 @@ +__author__ = 'wack' + +# part of the magwire package + +# calculate magnetic fields arising from electrical current through wires of arbitrary shape +# with the law of Biot-Savart + +# written by Michael Wack 2015 +# wack@geophysik.uni-muenchen.de + +# tested with python 3.4.3 + +# calculate fields needed for an palint oven + +import numpy as np +import matplotlib.pyplot as plt +from copy import deepcopy +import wire +import biotsavart + +# two solenoids + +w1 = wire.Wire(path=wire.Wire.SolenoidPath(radius=0.1, pitch=0.02, turns=20), discretization_length=0.01, current=10).Rotate(axis=(0, 1, 0), deg=90) +w2 = wire.Wire(path=wire.Wire.SolenoidPath(radius=0.1, pitch=0.02, turns=30), discretization_length=0.01, current=20).Rotate(axis=(0, 1, 0), deg=90).Translate([.45,0,0]) + + +sol = biotsavart.BiotSavart(wire=w1) +sol.AddWire(w2) + +resolution = 0.01 +xy_corner1 = (-.2, -.09) +xy_corner2 = (.8+1e-10, .09) + +# matplotlib plot 2D +# create list of xy coordinates +grid = np.mgrid[xy_corner1[0]:xy_corner2[0]:resolution, xy_corner1[1]:xy_corner2[1]:resolution] + +# create list of grid points +points = np.vstack(map(np.ravel, grid)).T +points = np.hstack([points, np.zeros([len(points),1])]) + +# calculate B field at given points +B = sol.CalculateB(points=points) +Babs = np.linalg.norm(B, axis=1) + +# remove big values close to the wire +#cutoff = 0.005 + +#B[Babs > cutoff] = [np.nan,np.nan,np.nan] +#Babs[Babs > cutoff] = np.nan + + + +#2d quiver +# get 2D values from one plane with Z = 0 + +fig = plt.figure() +ax = fig.gca() +ax.quiver(points[:, 0], points[:, 1], B[:, 0], B[:, 1], scale=.15) +X = np.unique(points[:, 0]) +Y = np.unique(points[:, 1]) +cs = ax.contour(X, Y, Babs.reshape([len(X), len(Y)]).T, 10) +ax.clabel(cs) +plt.xlabel('x') +plt.ylabel('y') +plt.axis('equal') +plt.show() + + +x_vals = np.arange(xy_corner1[0],xy_corner2[0],resolution) +points = np.array( [x_vals, x_vals*0, x_vals*0]).T +print(points) +B = sol.CalculateB(points=points) +Babs = np.linalg.norm(B, axis=1) + +fig = plt.figure() +plt.plot(points[:,0], Babs) +plt.xlabel('x[m]') +plt.ylabel('B[T]') +plt.show() + diff --git a/models/MagWire/sinusoidal_loop_demag.py b/models/MagWire/sinusoidal_loop_demag.py new file mode 100644 index 00000000..fa985b5d --- /dev/null +++ b/models/MagWire/sinusoidal_loop_demag.py @@ -0,0 +1,113 @@ +__author__ = 'wack' + + +# part of the magwire package + +# calculate magnetic fields arising from electrical current through wires of arbitrary shape +# with the law of Biot-Savart + +# written by Michael Wack 2015 +# wack@geophysik.uni-muenchen.de + +# tested with python 3.4.3 + + +# calculate magnetic field caused by two sinusoidal loops with opposing current directions + +import numpy as np + +try: + import visvis as vv + visvis_avail = True +except ImportError: + visvis_avail = False + print("visvis not found.") + +from matplotlib import pyplot as plt +from mpl_toolkits.mplot3d import axes3d + +import wire +import biotsavart + + +npts = 200 +radius = .07 +amp = .01 +ncycle = 12.0 + + +# define wires +w1 = wire.Wire(path=wire.Wire.SinusoidalCircularPath(radius=radius, amplitude=amp, frequency=ncycle, pts=npts), discretization_length=0.01, current=100.0) +w2 = wire.Wire(path=wire.Wire.SinusoidalCircularPath(radius=radius, amplitude=amp, frequency=ncycle, pts=npts), discretization_length=0.01, current=-100.0).Rotate(axis=(0, 0, 1), deg=360.0/ncycle/2.0) + +# prepare data grid and calculate B in volume +resolution = 0.005 +# volume to examine +volume_corner1 = (0, 0, 0) +volume_corner2 = (.12, .12, .05) + +grid3D = np.mgrid[volume_corner1[0]:volume_corner2[0]:resolution, volume_corner1[1]:volume_corner2[1]:resolution, volume_corner1[2]:volume_corner2[2]:resolution] +grid2D = np.mgrid[volume_corner1[0]:volume_corner2[0]:resolution/2.0, volume_corner1[1]:volume_corner2[1]:resolution/2.0, 0:1] + +# create list of grid points +points3D = np.vstack(map(np.ravel, grid3D)).T +points2D = np.vstack(map(np.ravel, grid2D)).T + +# calculate B field at given points +bs = biotsavart.BiotSavart(wire=w1) +bs.AddWire(w2) + +B = bs.CalculateB(points=points3D) +Babs = np.linalg.norm(B, axis=1) + +B2D = bs.CalculateB(points=points2D) +B2Dabs = np.linalg.norm(B2D, axis=1) +B2Dabs = B2Dabs.clip(0,0.01) +B2Dabsgrid = B2Dabs.reshape(grid2D.shape[1:-1]).T + + +# draw results + +# prepare axes +a = vv.gca() +a.cameraType = '3d' +a.daspectAuto = False + +#Bgrid = B.reshape(grid3D.shape).T +Babsgrid = Babs.reshape(grid3D.shape[1:]).T + +# clipping is not automatic! +Babsgrid = np.clip(Babsgrid, 0, 0.005) +Babsgrid = vv.Aarray(Babsgrid, sampling=(resolution, resolution, resolution), + origin=(volume_corner1[2], volume_corner1[1], volume_corner1[0])) + + +# set labels +vv.xlabel('x axis') +vv.ylabel('y axis') +vv.zlabel('z axis') + +bs.vv_PlotWires() + +t = vv.volshow2(Babsgrid, renderStyle='mip', cm=vv.CM_JET) +vv.colorbar() +app = vv.use() +app.Run() + + +# matplotlib plot + +fig = plt.figure() + +# 3d quiver + +#ax = fig.gca(projection='3d') +#ax.quiver(points[:, 0], points[:, 1], points[:, 2], B[:, 0], B[:, 1], B[:, 2], length=0.005) + +#2d quiver +ax = fig.gca() +ax.quiver(points2D[:, 0], points2D[:, 1], B2D[:, 0], B2D[:, 1], scale=.3) +cs = ax.contour(grid2D.reshape(grid2D.shape[:-1])[0], grid2D.reshape(grid2D.shape[:-1])[1], B2Dabsgrid) +ax.clabel(cs) + +plt.show() diff --git a/models/MagWire/solenoid_demo.py b/models/MagWire/solenoid_demo.py new file mode 100644 index 00000000..174e819e --- /dev/null +++ b/models/MagWire/solenoid_demo.py @@ -0,0 +1,91 @@ +__author__ = 'wack' + +# part of the magwire package + +# calculate magnetic fields arising from electrical current through wires of arbitrary shape +# with the law of Biot-Savart + +# written by Michael Wack 2015 +# wack@geophysik.uni-muenchen.de + +# tested with python 3.4.3 + +# some basic calculations for testing + +import numpy as np +import matplotlib.pyplot as plt +import wire +import biotsavart + + +# simple solenoid +# approximated analytical solution: B = mu0 * I * n / l = 4*pi*1e-7[T*m/A] * 100[A] * 10 / 0.5[m] = 2.5mT + + +w = wire.Wire(path=wire.Wire.SolenoidPath(pitch=0.05, turns=10), discretization_length=0.01, current=100).Rotate(axis=(1, 0, 0), deg=90) #.Translate((0.1, 0.1, 0)). +sol = biotsavart.BiotSavart(wire=w) + +resolution = 0.04 +volume_corner1 = (-.2, -.8, -.2) +volume_corner2 = (.2+1e-10, .3, .2) + +# matplotlib plot 2D +# create list of xy coordinates +grid = np.mgrid[volume_corner1[0]:volume_corner2[0]:resolution, volume_corner1[1]:volume_corner2[1]:resolution] + +# create list of grid points +points = np.vstack(map(np.ravel, grid)).T +points = np.hstack([points, np.zeros([len(points),1])]) + +# calculate B field at given points +B = sol.CalculateB(points=points) + + +Babs = np.linalg.norm(B, axis=1) + +# remove big values close to the wire +cutoff = 0.005 + +B[Babs > cutoff] = [np.nan,np.nan,np.nan] +#Babs[Babs > cutoff] = np.nan + +for ba in B: + print(ba) + +#2d quiver +# get 2D values from one plane with Z = 0 + +fig = plt.figure() +ax = fig.gca() +ax.quiver(points[:, 0], points[:, 1], B[:, 0], B[:, 1], scale=.15) +X = np.unique(points[:, 0]) +Y = np.unique(points[:, 1]) +cs = ax.contour(X, Y, Babs.reshape([len(X), len(Y)]).T, 10) +ax.clabel(cs) +plt.xlabel('x') +plt.ylabel('y') +plt.axis('equal') +plt.show() + + +# matplotlib plot 3D + +grid = np.mgrid[volume_corner1[0]:volume_corner2[0]:resolution*2, volume_corner1[1]:volume_corner2[1]:resolution*2, volume_corner1[2]:volume_corner2[2]:resolution*2] + +# create list of grid points +points = np.vstack(map(np.ravel, grid)).T + +# calculate B field at given points +B = sol.CalculateB(points=points) + +Babs = np.linalg.norm(B, axis=1) + +fig = plt.figure() +# 3d quiver +ax = fig.gca(projection='3d') +sol.mpl3d_PlotWires(ax) +ax.quiver(points[:, 0], points[:, 1], points[:, 2], B[:, 0], B[:, 1], B[:, 2], length=0.04) +plt.show() + + + diff --git a/models/MagWire/wire.py b/models/MagWire/wire.py new file mode 100644 index 00000000..c0d6a569 --- /dev/null +++ b/models/MagWire/wire.py @@ -0,0 +1,205 @@ +__author__ = 'wack' + +# part of the magwire package + +# calculate magnetic fields arising from electrical current through wires of arbitrary shape +# with the law of Biot-Savart + +# written by Michael Wack +# wack@geophysik.uni-muenchen.de + +# tested with python 3.4.3 + +from copy import deepcopy +import numpy as np +try: + import visvis as vv + visvis_avail = True +except ImportError: + visvis_avail = False + print("visvis not found.") + + +class Wire: + ''' + represents an arbitrary 3D wire geometry + ''' + def __init__(self, current=1, path=None, discretization_length=0.01): + ''' + + :param current: electrical current in Ampere used for field calculations + :param path: geometry of the wire specified as path of n 3D (x,y,z) points in a numpy array with dimension n x 3 + length unit is meter + :param discretization_length: lenght of dL after discretization + ''' + self.current = current + self.path = path + self.discretization_length = discretization_length + + + @property + def discretized_path(self): + ''' + calculate end points of segments of discretized path + approximate discretization lenghth is given by self.discretization_length + elements will never be combined + elements longer that self.dicretization_length will be divided into pieces + :return: discretized path as m x 3 numpy array + ''' + + try: + return self.dpath + except AttributeError: + pass + + self.dpath = deepcopy(self.path) + for c in range(len(self.dpath)-2, -1, -1): + # go backwards through all elements + # length of element + element = self.dpath[c+1]-self.dpath[c] + el_len = np.linalg.norm(element) + npts = int(np.ceil(el_len / self.discretization_length)) # number of parts that this element should be split up into + if npts > 1: + # element too long -> create points between + # length of new sub elements + sel = el_len / float(npts) + for d in range(npts-1, 0, -1): + self.dpath = np.insert(self.dpath, c+1, self.dpath[c] + element / el_len * sel * d, axis=0) + + return self.dpath + + @property + def IdL_r1(self): + ''' + calculate discretized path elements dL and their center point r1 + :return: numpy array with I * dL vectors, numpy array of r1 vectors (center point of element dL) + ''' + npts = len(self.discretized_path) + if npts < 2: + print("discretized path must have at least two points") + return + + IdL = np.array([self.discretized_path[c+1]-self.discretized_path[c] for c in range(npts-1)]) * self.current + r1 = np.array([(self.discretized_path[c+1]+self.discretized_path[c])*0.5 for c in range(npts-1)]) + + return IdL, r1 + + + def vv_plot_path(self, discretized=True, color='r'): + if not visvis_avail: + print("plot path works only with visvis module") + return + + if discretized: + p = self.discretized_path + else: + p = self.path + + vv.plot(p, ms='x', mc=color, mw='2', ls='-', mew=0) + + + def mpl3d_plot_path(self, discretized=True, show=True, ax=None, plt_style='-r'): + + if ax is None: + fig = plt.figure(None) + ax = ax3d.Axes3D(fig) + + if discretized: + p = self.discretized_path + else: + p = self.path + + ax.plot(p[:, 0], p[:, 1], p[:, 2], plt_style) + ax.set_xlabel('X') + ax.set_ylabel('Y') + ax.set_zlabel('Z') + + # make all axes the same + #max_a = np.array((p[:, 0], p[:, 1], p[:, 2])).max() + + #ax.set_xlim3d(min(p[:, 0]), max_a) + #ax.set_ylim3d(min(p[:, 1]), max_a) + #ax.set_zlim3d(min(p[:, 2]), max_a) + + + if show: + plt.show() + + return ax + + def ExtendPath(self, path): + ''' + extends existing path by another one + :param path: path to append + ''' + if self.path is None: + self.path = path + else: + # check if last point is identical to avoid zero length segments + if self.path[-1] == path[0]: + self.path=np.append(self.path, path[1:], axis=1) + else: + self.path=np.append(self.path, path, axis=1) + + def Translate(self, xyz): + ''' + move the wire in space + :param xyz: 3 component vector that describes translation in x,y and z direction + ''' + if self.path is not None: + self.path += np.array(xyz) + + return self + + def Rotate(self, axis=(1,0,0), deg=0): + ''' + rotate wire around given axis by deg degrees + :param axis: axis of rotation + :param deg: angle + ''' + if self.path is not None: + n = axis + ca = np.cos(np.radians(deg)) + sa = np.sin(np.radians(deg)) + R = np.array([[n[0]**2*(1-ca)+ca, n[0]*n[1]*(1-ca)-n[2]*sa, n[0]*n[2]*(1-ca)+n[1]*sa], + [n[1]*n[0]*(1-ca)+n[2]*sa, n[1]**2*(1-ca)+ca, n[1]*n[2]*(1-ca)-n[0]*sa], + [n[2]*n[0]*(1-ca)-n[1]*sa, n[2]*n[1]*(1-ca)+n[0]*sa, n[2]**2*(1-ca)+ca]]) + self.path = np.dot(self.path, R.T) + + return self + + + + # different standard paths + @staticmethod + def LinearPath(pt1=(0, 0, 0), pt2=(0, 0, 1)): + return np.array([pt1, pt2]).T + + @staticmethod + def RectangularPath(dx=0.1, dy=0.2): + dx2 = dx/2.0; dy2 = dy/2.0 + return np.array([[dx2, dy2, 0], [dx2, -dy2, 0], [-dx2, -dy2, 0], [-dx2, dy2, 0], [dx2, dy2, 0]]).T + + @staticmethod + def CircularPath(radius=0.1, pts=20): + return Wire.EllipticalPath(rx=radius, ry=radius, pts=pts) + + @staticmethod + def SinusoidalCircularPath(radius=0.1, amplitude=0.01, frequency=10, pts=100): + t = np.linspace(0, 2 * np.pi, pts) + return np.array([radius * np.sin(t), radius * np.cos(t), amplitude * np.cos(frequency*t)]).T + + @staticmethod + def EllipticalPath(rx=0.1, ry=0.2, pts=20): + t = np.linspace(0, 2 * np.pi, pts) + return np.array([rx * np.sin(t), ry * np.cos(t), 0]).T + + @staticmethod + def SolenoidPath(radius=0.1, pitch=0.01, turns=30, pts_per_turn=20): + return Wire.EllipticalSolenoidPath(rx=radius, ry=radius, pitch=pitch, turns=turns, pts_per_turn=pts_per_turn) + + @staticmethod + def EllipticalSolenoidPath(rx=0.1, ry=0.2, pitch=0.01, turns=30, pts_per_turn=20): + t = np.linspace(0, 2 * np.pi * turns, pts_per_turn * turns) + return np.array([rx * np.sin(t), ry * np.cos(t), t / (2 * np.pi) * pitch]).T + diff --git a/models/solenoids/.gitignore b/models/solenoids/.gitignore new file mode 100644 index 00000000..82b191b2 --- /dev/null +++ b/models/solenoids/.gitignore @@ -0,0 +1,198 @@ + +# Created by https://www.gitignore.io/api/python,pycharm +# Edit at https://www.gitignore.io/?templates=python,pycharm + +### PyCharm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PyCharm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +.idea/**/sonarlint/ + +# SonarQube Plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator/ + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# End of https://www.gitignore.io/api/python,pycharm \ No newline at end of file diff --git a/models/solenoids/.idea/.idea/.idea.iml b/models/solenoids/.idea/.idea/.idea.iml new file mode 100644 index 00000000..d0876a78 --- /dev/null +++ b/models/solenoids/.idea/.idea/.idea.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/models/solenoids/.idea/.idea/inspectionProfiles/profiles_settings.xml b/models/solenoids/.idea/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..105ce2da --- /dev/null +++ b/models/solenoids/.idea/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/models/solenoids/.idea/.idea/misc.xml b/models/solenoids/.idea/.idea/misc.xml new file mode 100644 index 00000000..a2e120dc --- /dev/null +++ b/models/solenoids/.idea/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/models/solenoids/.idea/.idea/modules.xml b/models/solenoids/.idea/.idea/modules.xml new file mode 100644 index 00000000..08f54a6d --- /dev/null +++ b/models/solenoids/.idea/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/models/solenoids/.idea/.idea/workspace.xml b/models/solenoids/.idea/.idea/workspace.xml new file mode 100644 index 00000000..6b59c0d1 --- /dev/null +++ b/models/solenoids/.idea/.idea/workspace.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/models/solenoids/.idea/inspectionProfiles/profiles_settings.xml b/models/solenoids/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..105ce2da --- /dev/null +++ b/models/solenoids/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/models/solenoids/.idea/misc.xml b/models/solenoids/.idea/misc.xml new file mode 100644 index 00000000..a2e120dc --- /dev/null +++ b/models/solenoids/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/models/solenoids/.idea/modules.xml b/models/solenoids/.idea/modules.xml new file mode 100644 index 00000000..aab61eee --- /dev/null +++ b/models/solenoids/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/models/solenoids/.idea/solenoids.iml b/models/solenoids/.idea/solenoids.iml new file mode 100644 index 00000000..67116063 --- /dev/null +++ b/models/solenoids/.idea/solenoids.iml @@ -0,0 +1,11 @@ + + + + + + + + + + \ No newline at end of file diff --git a/models/solenoids/.idea/vcs.xml b/models/solenoids/.idea/vcs.xml new file mode 100644 index 00000000..b2bdec2d --- /dev/null +++ b/models/solenoids/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/models/solenoids/GUIscratch.py b/models/solenoids/GUIscratch.py new file mode 100644 index 00000000..e5c205f1 --- /dev/null +++ b/models/solenoids/GUIscratch.py @@ -0,0 +1,117 @@ +from tkinter import * +from tkinter import ttk +from em_force_model import * + +class solenoidGUI: + + def __init__(self, master): + self.master = master + master.title("window title") + # geometry: width x height + master.geometry('1080x720') + self.description_label = Label(master, text="Solenoid Model", font=("Arial Bold", 40)).grid( + column=0, row=0) + + # Changeable Parameters + self.description_label = Label(master, text="Changeable parameters: geometry", font=("Arial Bold", 20)).grid( + column=0, row=1) + frame = Frame(master) + frame.grid(column=0, row=2) + + self.description_length_solenoid = Label(frame, text="Solenoid Length in mm").grid(column=0, row=1) + self.length_solenoid = Entry(frame, width=10) + self.length_solenoid.grid(column=1, row=1) + + self.description_gauge_thickness = Label(frame, text="Gauge Thickness in mm").grid(column=0, row=2) + + self.gauge_thickness = ttk.Combobox(frame, values=["12", "13", "14", "15"]) + self.gauge_thickness.current(0) # set the selected item + self.gauge_thickness.grid(column=1, row=2) + + # if circle, square, show + self.description_diameter = Label(frame, text="Diameter (square, circular)").grid(column=0, row=3) + self.diameter = Entry(frame, width=10) + self.diameter.grid(column=1, row=3) + + # if rectangle, oval, show + self.description_side_long = Label(frame, text="Length of longer side").grid(column=0, row=4) + self.side_long = Entry(frame, width=10) + self.side_long.grid(column=1, row=4) + + self.description_side_short = Label(frame, text="Length of shorter side").grid(column=0, row=5) + self.side_short = Entry(frame, width=10) + self.side_short.grid(column=1, row=5) + + # External Values, changeable, but dependent on other components + self.description_label2 = Label(master, text="Parameters: external circuit", font=("Arial Bold", 20)).grid( + column=0, row=3) + frame2 = Frame(master) + frame2.grid(column=0, row=4) + + self.description_voltage = Label(frame2, text="Voltage in V").grid(column=0, row=1) + self.voltage = Entry(frame2, width=10) + self.voltage.grid(column=1, row=1) + + self.description_resistance = Label(frame2, text="resistance in ohms").grid(column=0, row=2) + self.resistance = Entry(frame2, width=10) + self.resistance.grid(column=1, row=2) + + self.input_shape = ttk.Combobox(frame, values=["rectangle", "circle", "ellipse", "square"]) + self.input_shape.current(0) # set the selected item + self.input_shape.grid(column=0, row=0) + + # click to solve + self.button = Button(master, text='Solve', command=self.calc) + self.button.grid(column=0, row=9) + self.close_button = Button(master, text="Close", command=master.quit).grid(column=0, row=10) + # output + self.output_label = Label(master, text="output here") + self.output_label.grid(column=0, row=11) + + def calc(self): + + # relating the combobox index to text + rectangle = 0 + circle = 1 + ellipse = 2 + square = 3 + + n = num_of_loops(float(self.length_solenoid.get()), float(self.gauge_thickness.get())) + i = max_current(float(self.voltage.get()), float(self.resistance.get())) + + if self.input_shape.current() == rectangle: + area = cross_section_area_rect(float(self.side_short.get()), float(self.side_long.get())) + mf = calc_mf_rect_solenoid(n, i, float(self.side_short.get()), float(self.side_long.get())) + printed = calc_force(area, mf) + self.output_label.configure(text=printed) + + if self.input_shape.current() == circle: + area = cross_section_area_circle(float(self.diameter.get())) + mf = calc_mf_circular_solenoid(n, i, float(self.diameter.get())) + printed = calc_force(area, mf) + self.output_label.configure(text=printed) + + '''if self.input_shape.current() == ellipse: + area = cross_section_area_ellipse() + mf = calc_mf_ellipse_solenoid() + printed = calc_force(area, mf) + self.output_label.configure(text=printed)''' + + if self.input_shape.current() == square: + area = cross_section_area_square(float(self.diameter.get())) + mf = calc_mf_square_solenoid(n, i, float(self.diameter.get())) + printed = calc_force(area, mf) + self.output_label.configure(text=printed) + + + +if __name__ == '__main__': + root = Tk() + my_gui = solenoidGUI(root) + root.mainloop() + # AWG conversions to mm + import json + + with open('awg_data.json', 'r') as awg_json: + awg_data = json.load(awg_json) + print(awg_data['AWG']) diff --git a/models/solenoids/awg_data.json b/models/solenoids/awg_data.json new file mode 100644 index 00000000..7eb4a51d --- /dev/null +++ b/models/solenoids/awg_data.json @@ -0,0 +1,55 @@ +{ + "AWG": { + "12": { + "mm": "2.05232" + }, + "13": { + "mm": "1.8288" + }, + "14": { + "mm": "1.62814" + }, + "15": { + "mm": "1.45034" + }, + "16": { + "mm": "1.29032" + }, + "17": { + "mm": "1.15062" + }, + "18": { + "mm": "1.02362" + }, + "19": { + "mm": "0.91186" + }, + "20": { + "mm": "0.8128" + }, + "21": { + "mm": "0.7239" + }, + "22": { + "mm": "0.64516" + }, + "23": { + "mm": "0.57404" + }, + "24": { + "mm": "0.51054" + }, + "25": { + "mm": "0.45466" + }, + "26": { + "mm": "0.40386" + }, + "27": { + "mm": "0.36068" + }, + "28": { + "mm": "0.32004" + } + } +} \ No newline at end of file diff --git a/models/solenoids/em_force_model.py b/models/solenoids/em_force_model.py new file mode 100644 index 00000000..928390ca --- /dev/null +++ b/models/solenoids/em_force_model.py @@ -0,0 +1,144 @@ +# Purpose: Calculating the magnetic field strength at the center of the solenoid using Biot Savart Law +# This value is then used as an input to determine the electromagnetic force. +# For more information refer to 'Solenoid Research' (google docs) + +# variables declarations used in this program are categorized into ... +# . values to be determined by design, values to be changed during play, output/calculated values +from typing import Dict +import scipy +from scipy import constants +import matplotlib.pyplot as plt +import numpy as np + + +# __all__ = [constants] + + +def input_values(): + parameter_list = { + 'length_solenoid': 10, # length of solenoid + 'wire_gauge': 1, # wire gauge + 'diameter': 1, # diameter of circular, side length of square + 'side_long': 2, # longer side for rect, longer value for ellipse + 'side_short': 1, # shorter side for rect, shorter value for ellipse + + # External Values, changeable, but dependent on other components + + 'max_voltage': 4, # the voltage across the capacitor + 'resistance': 1, # resistance of the device + 'capacitance': 3, # capacitance of our capacitor + + # input values, changes to model (visually) + + 'total_time': 5., # the time frame we wish to look at + 'time_interval': 0.2, # ### we want to change length of pulse + } + return parameter_list + + +def time_constant(resistance, capaciatance): + tc = resistance * capaciatance + return tc + + +def max_current(max_voltage, resistance): + mc = max_voltage / resistance + return mc + + # for i in range(0, parameter_list['total_time']): + # current_at_t.append(current(max_current, time_elapsed, time_constant)) + # i+time_elapsed + + +def num_of_loops(solenoid_length, gauge_thickness): + n = solenoid_length / gauge_thickness + return n + + +# magnetic field (mf) calculations and area calculations + + +def calc_mf_circular_solenoid(num_loops, current, diameter): + magnetic_field = scipy.constants.mu_0 * num_loops * current / diameter + return magnetic_field + + +def cross_section_area_circle(dia): # area calculation (cross section of solenoid) + area = scipy.power((dia / 2), 2) * scipy.pi + return area + + +def calc_mf_square_solenoid(num_loops, current, side_length): + magnetic_field = np.sqrt(2) * scipy.constants.mu_0 * num_loops * current / (scipy.pi * side_length / 2) + return magnetic_field + + +def cross_section_area_square(side_length): # area calculation (cross section of solenoid) + area = side_length * side_length + return area + + +def calc_mf_rect_solenoid(num_loops, current, length_short, length_long): + magnetic_field = (2 * scipy.constants.mu_0 * num_loops * current / scipy.pi) * ( + length_long / ( + length_short * np.sqrt(scipy.power(length_long, 2) + scipy.power(length_short, 2))) + length_short / + (length_long * np.sqrt(scipy.power(length_long, 2) + scipy.power(length_short, 2)))) + return magnetic_field + + +def cross_section_area_rect(length_long, length_short): # area calculation (cross section of solenoid) + area = length_short * length_long + return area + + +def calc_mf_ellipse_solenoid(num_loops, current, length_short, length_long): # ****needs work + magnetic_field = 1 + return magnetic_field + + +def cross_section_area_ellipse(length_long, length_short): # area calculation (cross section of solenoid) + # area = 1 * 1 + return area + + +def calc_force(area, mf): + force = area * scipy.power(mf, 2) / 2 / scipy.constants.mu_0 + return force + + +'''# time_elapsed = np.arange(0., parameter_list['total_time'], parameter_list['time_interval']) + + # plt.plot(time_elapsed, current(max_current, time_elapsed, time_constant), 'b--') + + plt.plot(time_elapsed, calculate_field_strength_at_p(calculate_constant_multiplier(parameter_list['num_coils'], + current(max_current, + time_elapsed, + time_constant), + parameter_list[ + 'length_solenoid'], + parameter_list['outer_diameter'], + parameter_list[ + 'inner_diameter']), + length_add_distance_from_center, + ln_term_a, + length_subtract_distance_from_center, + ln_term_b), + 'b--') + + plt.show()''' + +## + +# FUNCTIONS TO BE CALLED FROM MAIN FUNCTION +# THIS WAS DONE TO REDUCE CLUTTER IN THE MAIN FUNCTION AND TO CREATE LESS COMPLEX UNIT TEST CASES FOR THE FUNCTIONS + +## + +'''def current(max_current, time_elapsed, time_constant): + cur = max_current * scipy.power(scipy.e, -time_elapsed / time_constant) + return cur + +''' + +# if __name__ == "__main__": +# input_values() \ No newline at end of file diff --git a/models/solenoids/requirements.txt b/models/solenoids/requirements.txt new file mode 100644 index 00000000..e9b621ff --- /dev/null +++ b/models/solenoids/requirements.txt @@ -0,0 +1 @@ +numpy==1.17.3 diff --git a/models/solenoids/test_em_force_model.py b/models/solenoids/test_em_force_model.py new file mode 100644 index 00000000..1c787f94 --- /dev/null +++ b/models/solenoids/test_em_force_model.py @@ -0,0 +1,29 @@ +from unittest import TestCase +from .em_force_model import em_force_model, calculate_remanence + + +class EmForceModelTestCase(TestCase): + def __init__(self, *args): + super(EmForceModelTestCase, self).__init__(*args) + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_calculate_remanence_test_case(self): + current = 0.1 # Amps + length_solenoid = 10 # + num_turns = 50 # + val = calculate_remanence( + current, + length_solenoid, + num_turns + ) + self.assertNotEqual(val, 1, "Expected not equal") + + + + + def test_ \ No newline at end of file